-
Notifications
You must be signed in to change notification settings - Fork 358
Channel link autocomplete #1902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Exciting! Thanks for building this. Just to record here what I said on the team call yesterday: for this PR, we can start the reviews in parallel with you writing the tests. So I'd suggest going ahead and adding those docs and comments next — then once you consider the PR all ready except for the tests, just mention that here and add the "maintainer review" label. |
5a8171d to
6671cfa
Compare
|
Thanks @gnprice for mentioning this. This is now ready for an initial review. (While working on the first todo, I realized that there were a few other places that needed some changes, which caused a delay 😀) |
6671cfa to
fe25a32
Compare
|
(just rebased atop main with conflicts resolved) |
fe25a32 to
480e787
Compare
6b2fb06 to
5072dce
Compare
|
@chrisbobbe Pushed a new revision with tests included. |
|
Thanks for this, and apologies for my delay in reviewing! Here's a review of the first six commits: 05c8437 channel: Add isRecentlyActive field plus some comments on later commits where I happened to see something interesting. 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh no—thanks for the ping in DMs, somehow I didn't actually submit that review yesterday! Here it is.
test/example_data.dart
Outdated
| historyPublicToSubscribers: historyPublicToSubscribers ?? true, | ||
| messageRetentionDays: messageRetentionDays, | ||
| channelPostPolicy: channelPostPolicy ?? ChannelPostPolicy.any, | ||
| isRecentlyActive: isRecentlyActive ?? false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think true might be a more natural default value for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to true. The false default value was to make the tests in "ranking across signals" and "final results end-to-end" of "ChannelLinkAutocompleteView" group a little less verbose.🙂
lib/api/model/model.dart
Outdated
| @JsonKey(name: 'stream_post_policy') | ||
| ChannelPostPolicy? channelPostPolicy; // TODO(server-10) remove | ||
| // final bool isAnnouncementOnly; // deprecated for `channelPostPolicy`; ignore | ||
| bool? isRecentlyActive; // TODO(server-10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
channel: Add isRecentlyActive field
Since we're already not matching the order in the API doc (see e.g. #1894 (comment) ), I'd put this next to the related-looking field streamWeeklyTraffic, perhaps just above it without an empty line in between.
Similarly elsewhere in this commit.
lib/api/model/events.dart
Outdated
| required super.id, | ||
| required this.streams, | ||
| required this.streamIds, | ||
| }) : assert(streams != null || streamIds != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We try to reserve assert for invariants that are our own responsibility, i.e. those that won't be broken except for some broken logic in the client. Here, the invariant exists, but it's one that can be broken by something out of our control, in particular a badly-behaving server.
Also, asserts don't run in production, so this won't work as "crunchy shell" validation. It makes sense to want such validation, so the "soft center" of the app can rely on this invariant. But let's do it in ChannelDeleteEvent.fromJson; for an example to follow, see DeleteMessageEvent.fromJson.
lib/api/model/events.dart
Outdated
|
|
||
| final List<ZulipStream> streams; | ||
| final List<ZulipStreamId>? streams; // TODO(server-10): remove | ||
| final List<int>? streamIds; // TODO(server-10): remove nullability |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we'd normally say TODO(server-10) make required or just TODO(server-10).
lib/model/channel.dart
Outdated
| streams.remove(stream.streamId); | ||
| streamsByName.remove(stream.name); | ||
| subscriptions.remove(stream.streamId); | ||
| final channelIds = event.streamIds ?? event.streams!.map((e) => e.streamId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh actually, we can make this nicer by encapsulating this conditional in the API-binding layer—ChannelDeleteEvent can have just final List<int> streamIds (maybe channelIds, as the more modern name?), and it can read its value depending on what the JSON looks like.
Something like this (untested)?:
/// A [ChannelEvent] with op `delete`: https://zulip.com/api/get-events#stream-delete
@JsonSerializable(fieldRename: FieldRename.snake)
class ChannelDeleteEvent extends ChannelEvent {
@override
@JsonKey(includeToJson: true)
String get op => 'delete';
@JsonKey(readValue: _readChannelIds)
final List<int> channelIds;
// TODO(server-10) simplify away; rely on stream_ids
static List<int> _readChannelIds(Map<dynamic, dynamic> json, String key) {
final channelIds = json['stream_ids'] as List<int>?;
if (channelIds != null) return channelIds;
final channels = json['streams'] as List<dynamic>;
return channels
.map((c) => (c as Map<String, dynamic>)['stream_id'] as int)
.toList();
}
ChannelDeleteEvent({
required super.id,
required this.channelIds,
});
factory ChannelDeleteEvent.fromJson(Map<String, dynamic> json) =>
_$ChannelDeleteEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$ChannelDeleteEventToJson(this);
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(In that code, the crunchy-shell validation is done by the final channels = json['streams'] as List<dynamic>; line, which will throw if both .stream_ids and .streams are absent in the json.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to the new version. One thing that this does is that in toJson, there will be an entry with key channel_ids; not exactly what the server gives us (stream_ids or streams). Should we edit the toJson method to match the server data, or is it not that important since we don't use it to send it back to the server?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, so there was a test failing in test/model/store_test.dart and the fix was to include streams in toJson.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, streams is deprecated and may be removed, so I think we should treat it as valid if streams is absent and stream_ids is present. I'd want our tests to tolerate that form without failing.
I took a look and found a bug in _readChannelIds in this revision 🙂:
diff --git lib/api/model/events.dart lib/api/model/events.dart
index 6a0d9ffa4..4d9c5121c 100644
--- lib/api/model/events.dart
+++ lib/api/model/events.dart
@@ -622,7 +622,7 @@ class ChannelDeleteEvent extends ChannelEvent {
// TODO(server-10) simplify away; rely on stream_ids
static List<int> _readChannelIds(Map<dynamic, dynamic> json, String key) {
final channelIds = json['stream_ids'] as List<dynamic>?;
- if (channelIds != null) channelIds.map((id) => id as int).toList();
+ if (channelIds != null) return channelIds.map((id) => id as int).toList();
final channels = json['streams'] as List<dynamic>;
return channels
lib/widgets/autocomplete.dart
Outdated
| overflow: TextOverflow.ellipsis, | ||
| color: designVariables.contextMenuItemMeta)), | ||
| child: BlockContentList( | ||
| nodes: parseContent(channel!.renderedDescription).nodes), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! Unfortunately we're not ready to show the rendered channel description; some of our content widgets will break if they appear outside the context of a message, because they use InheritedMessage.of(context), and we need to address that systematically, which is #488. See related issues:
- Show channel description in channel action sheet #1896
- content: Support Zulip content outside messages (even outside per-account contexts) #488
For now let's do as I did in #1877:
- Not try to show the channel description here
- File an issue for it and leave a TODO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #1945. Also, looking at https://github.com/zulip/zulip-flutter/blob/main/lib/widgets/content.dart, it seems like InheritedMessage.of(context) is used in two places, MessageImagePreview and MessageInlineVideo, and by manually testing, it seems like the server is not sending the related HTML for rendering these widgets when there is an image or video in the channel description. So I think it will be safe to show the channel description now. But as it’s possible that InheritedMessage.of(context) will be used in other widgets, it's good to wait for #488 as you mentioned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by manually testing, it seems like the server is not sending the related HTML for rendering these widgets when there is an image or video in the channel description. So I think it will be safe to show the channel description now.
For this sort of detail I'd want to rely on API guarantees rather than manual testing on a current server. It's at least plausible that we could run into different behavior with some old server we still support (like 7), or even on any server, including a modern one like CZO, for a channel that was last updated when the server version was ancient, like 3 or something.
But as it’s possible that
InheritedMessage.of(context)will be used in other widgets, it's good to wait for #488 as you mentioned.
Yep, this is also true :) it'll be nice to be systematic about it.
| // Behavior we have that web doesn't and might like to follow: | ||
| // - A "word-prefixes" match quality on channel names: | ||
| // see [NameMatchQuality.wordPrefixes], which we rank on. | ||
| // | ||
| // Behavior web has that seems undesired, which we don't plan to follow: | ||
| // - A "word-boundary" match quality on channel names: | ||
| // special rank when the whole query appears contiguously | ||
| // right after a word-boundary character. | ||
| // Our [NameMatchQuality.wordPrefixes] seems smarter. | ||
| // - Ranking some case-sensitive matches differently from case-insensitive | ||
| // matches. Users will expect a lowercase query to be adequate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I reading web's sort_streams correctly that it also considers channel descriptions in the filtering and ranking? I don't personally think we need to do that, but it probably deserves a mention here.
lib/model/autocomplete.dart
Outdated
| return switch((tryCast<Subscription>(a), tryCast<Subscription>(b))) { | ||
| (Subscription(), null) => -1, | ||
| (null, Subscription()) => 1, | ||
| (Subscription(isMuted: false), Subscription(isMuted: true)) => -1, | ||
| (Subscription(isMuted: true), Subscription(isMuted: false)) => 1, | ||
| (Subscription(pinToTop: true), Subscription(pinToTop: false)) => -1, | ||
| (Subscription(pinToTop: false), Subscription(pinToTop: true)) => 1, | ||
| _ => 0, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to add and use tryCast for this, and instead do something like:
| return switch((tryCast<Subscription>(a), tryCast<Subscription>(b))) { | |
| (Subscription(), null) => -1, | |
| (null, Subscription()) => 1, | |
| (Subscription(isMuted: false), Subscription(isMuted: true)) => -1, | |
| (Subscription(isMuted: true), Subscription(isMuted: false)) => 1, | |
| (Subscription(pinToTop: true), Subscription(pinToTop: false)) => -1, | |
| (Subscription(pinToTop: false), Subscription(pinToTop: true)) => 1, | |
| _ => 0, | |
| }; | |
| if (a is Subscription && b is! Subscription) return -1; | |
| if (a is! Subscription && b is Subscription) return 1; | |
| return switch((a, b)) { | |
| (Subscription(isMuted: false), Subscription(isMuted: true)) => -1, | |
| (Subscription(isMuted: true), Subscription(isMuted: false)) => 1, | |
| (Subscription(pinToTop: true), Subscription(pinToTop: false)) => -1, | |
| (Subscription(pinToTop: false), Subscription(pinToTop: true)) => 1, | |
| _ => 0, | |
| }; |
which is equivalent and doesn't add a step for the reader to interpret where null comes from and what it means. It also separates the main, headline logic from the rest, corresponding to the dartdoc's choice of what goes in its first line vs. the body:
/// Comparator that puts subscribed channels before unsubscribed ones.
///
/// For subscribed channels, it puts them in the following way:
/// pinned unmuted > unpinned unmuted > pinned muted > unpinned mutedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(also nit: s/way/order/ in that dartdoc)
lib/model/autocomplete.dart
Outdated
| /// Comparator that puts channels with more weekly traffic first. | ||
| /// | ||
| /// A channel with undefined weekly traffic (`null`) is put after the channel | ||
| /// with a weekly traffic defined (even if it is zero). | ||
| /// | ||
| /// Weekly traffic is the average number of messages sent to the channel per | ||
| /// week, which is determined by [ZulipStream.streamWeeklyTraffic]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// Comparator that puts channels with more weekly traffic first. | |
| /// | |
| /// A channel with undefined weekly traffic (`null`) is put after the channel | |
| /// with a weekly traffic defined (even if it is zero). | |
| /// | |
| /// Weekly traffic is the average number of messages sent to the channel per | |
| /// week, which is determined by [ZulipStream.streamWeeklyTraffic]. | |
| /// Comparator that puts channels with more [ZulipStream.streamWeeklyTraffic] first. | |
| /// | |
| /// A channel with undefined weekly traffic (`null`) is put after the channel | |
| /// with a weekly traffic defined (even if it is zero). |
This is a very reasonable definition of "weekly traffic" 🙂 and so isn't likely to bitrot i.e. become incorrect over time. But since we're just using ZulipStream.streamWeeklyTraffic directly (no computations on it), let's leave that field's definition as the single place where we write its definition, so we only have to change that one thing if the meaning changes.
…I see that we haven't actually written down the field's meaning, which we might've done in dartdoc on the field. But that's quite normal and appropriate; by leaving it blank, we mean to defer to the API documentation, which is linked in the class ZulipStream dartdoc.
test/model/compose_test.dart
Outdated
| eg.stream(streamId: 5, name: 'UI [v2]'), | ||
| eg.stream(streamId: 6, name: r'Save $$'), | ||
| ]; | ||
| store.addStreams(channels); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be awaited; similarly in a few other places.
Thanks to the discarded_futures lint for catching this, actually; I was playing with it for #731 this morning 🙂
8cde339 to
ea64c45
Compare
|
Thanks @chrisbobbe for the review. Pushed new changes, PTAL. |
ea64c45 to
d1abf20
Compare
chrisbobbe
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This is working great; comments below, this time from reading the whole branch.
lib/api/model/events.dart
Outdated
| case ChannelPropertyName.isRecentlyActive: | ||
| return value as bool?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: bump on #1902 (comment)
test/example_data.dart
Outdated
| case ChannelPropertyName.isRecentlyActive: | ||
| assert(value is bool?); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: bump on #1902 (comment)
lib/model/channel.dart
Outdated
| streams.remove(stream.streamId); | ||
| streamsByName.remove(stream.name); | ||
| subscriptions.remove(stream.streamId); | ||
| final channelIds = event.streamIds ?? event.streams!.map((e) => e.streamId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, streams is deprecated and may be removed, so I think we should treat it as valid if streams is absent and stream_ids is present. I'd want our tests to tolerate that form without failing.
I took a look and found a bug in _readChannelIds in this revision 🙂:
diff --git lib/api/model/events.dart lib/api/model/events.dart
index 6a0d9ffa4..4d9c5121c 100644
--- lib/api/model/events.dart
+++ lib/api/model/events.dart
@@ -622,7 +622,7 @@ class ChannelDeleteEvent extends ChannelEvent {
// TODO(server-10) simplify away; rely on stream_ids
static List<int> _readChannelIds(Map<dynamic, dynamic> json, String key) {
final channelIds = json['stream_ids'] as List<dynamic>?;
- if (channelIds != null) channelIds.map((id) => id as int).toList();
+ if (channelIds != null) return channelIds.map((id) => id as int).toList();
final channels = json['streams'] as List<dynamic>;
return channels| for (final view in _channelLinkAutocompleteViews) { | ||
| view.reassemble(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! Sounds like that would help us avoid a bug like the one fixed here.
lib/model/autocomplete.dart
Outdated
| // Similar reasoning as in _mentionIntentRegex. | ||
| const before = r'(?<=^|\s|\p{Punctuation})'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // As Web, match both '#channel' and '#**channel'. In both cases, the raw | ||
| // query is going to be 'channel'. Matching the second case ('#**channel') | ||
| // is useful when the user selects a channel from the autocomplete list, but | ||
| // then starts pressing "backspace" to edit the query and choose another | ||
| // option, instead of clearing the entire query and starting from scratch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Looks like web also does the corresponding thing for @-mentions: https://github.com/zulip/zulip/blob/1e78447c5/web/src/composebox_typeahead.ts#L516-L531
function filter_mention_name(current_token: string): string | undefined {
if (current_token.startsWith("**")) {
current_token = current_token.slice(2);
} else if (current_token.startsWith("*")) {
current_token = current_token.slice(1);
}
if (current_token.includes("*")) {
return undefined;
}
// Don't autocomplete if there is a space following an '@'
if (current_token.startsWith(" ")) {
return undefined;
}
return current_token;
}This is maybe more important for channel/topic autocomplete, right? Because (once the topic part is implemented) you might backspace as part of figuring out how to get just a channel link without a topic. But this is a good prompt to file an issue and add a TODO for doing this with @-mention autocomplete; would you do those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that would be an improvement. I have always missed that feature for @-mentions; filed #1967.
| } else { | ||
| icon = null; | ||
| iconColor = null; | ||
| label = zulipLocalizations.unknownChannelName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we normally show "(unknown channel)" in italics, to distinguish it from a potential channel with that name.
lib/widgets/autocomplete.dart
Outdated
| padding: EdgeInsetsDirectional.fromSTEB(12, 4, 10, 4), | ||
| child: Row(spacing: 10, children: [ | ||
| SizedBox.square(dimension: 24, | ||
| child: Icon(size: 18, color: iconColor, icon)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rows have 44px height in the Figma, but this gives 32px height (unless increased via the device text-size setting).
To bring it up to 44px, we could structure it similarly to MentionAutocompleteItem—
| padding: EdgeInsetsDirectional.fromSTEB(12, 4, 10, 4), | |
| child: Row(spacing: 10, children: [ | |
| SizedBox.square(dimension: 24, | |
| child: Icon(size: 18, color: iconColor, icon)), | |
| padding: EdgeInsetsDirectional.fromSTEB(4, 4, 8, 4), | |
| child: Row(spacing: 6, children: [ | |
| SizedBox.square(dimension: 36, | |
| child: Center( | |
| child: Icon(size: 18, color: iconColor, icon))), |
—which could be helpful in a potential future where we made a generic AutocompleteItem widget that serves both kinds of autocomplete.
lib/model/compose.dart
Outdated
| /// | ||
| /// [fallbackMarkdownLink] will be used if the channel name includes some faulty | ||
| /// characters that will break normal #**channel** rendering. | ||
| String channelLinkSyntax(ZulipStream channel, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just channelLink, I think; the "syntax" part is implied. (As in existing functions in this file, like quoteAndReply which creates quote-and-reply syntax, or userMention which creates user-mention syntax.)
lib/model/compose.dart
Outdated
| /// Markdown link for channel, topic, or message when the channel or topic name | ||
| /// includes characters that will break normal markdown rendering. | ||
| /// | ||
| /// Refer to [_channelTopicFaultyCharsReplacements] for a complete list of | ||
| /// these characters. | ||
| // Corresponds to `topic_link_util.get_fallback_markdown_link` in Zulip web; | ||
| // https://github.com/zulip/zulip/blob/b42d3e77e/web/src/topic_link_util.ts#L96-L108 | ||
| String fallbackMarkdownLink({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
channel: Finish channel link autocomplete for compose box
Could you separate this special character-replacement logic into its own commit? I want to see if we can ground our reasoning in API documentation. As far as that goes, there's nothing "invalid" or "faulty" about these characters appearing in channel names.
d1abf20 to
acb6615
Compare
|
Thanks @chrisbobbe for the previous review. Pushed a new revision, with some new commits added, PTAL. 65ad611 api: Add InitialSnapshot.maxChannelNameLength (The media in the PR description has also been updated.) |
acb6615 to
513bc80
Compare
|
Ah I see this has gathered some conflicts; would you resolve those please? |
513bc80 to
6d28107
Compare
gnprice
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Comments below on those same first 10 commits.
test/model/autocomplete_test.dart
Outdated
| group('ranking', () { | ||
| // This gets filled here at the start of the group, but never reset. | ||
| // We're counting on this group's tests never doing anything to mutate it. | ||
| store = eg.store(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment isn't right, because this is setting the store local that was declared in the outer group. That one gets overwritten in that group's other tests above.
In general it's best if the code outside of test cases (outside of test bodies) never creates large objects like instances of PerAccountStore. This code is declaring the tests, not part of running any specific test, so it runs unconditionally even when you use --name to filter down which tests run.
Hence why the "MentionAutocompleteQuery ranking" group (a) declares its own store local, (b) leaves it to be initialized lazily by a test case.
test/model/autocomplete_test.dart
Outdated
| group('compareChannelsByName', () { | ||
| int compare(String a, String b) => ChannelStore.compareChannelsByName( | ||
| eg.stream(name: a), eg.stream(name: b)); | ||
|
|
||
| test("favor channel with name coming first", () async { | ||
| check(compare('announce', 'backend')).isLessThan(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(cont'd from #1902 (comment))
This is a test for ChannelStore.compareChannelsByName. But it still doesn't test that the channel-link autocomplete code ever actually compares by name 🙂
There should be a test here that would fail if this line in the implementation:
return ChannelStore.compareChannelsByName(a, b);were removed (replaced with return 0;).
Conversely there's no need for this test. That's the job of tests in channel_test.dart.
| // The composing-to channel ranks last on each of the other criteria, | ||
| // but comes out first in the end, showing that composing-to channel | ||
| // comes first. Then among the remaining channels, the subscribed ones | ||
| // rank last on each of the remaining criteria, but comes out top | ||
| // in the end; and so on. | ||
| final channels = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, this structure looks right.
test/model/autocomplete_test.dart
Outdated
| // in the end; and so on. | ||
| final channels = [ | ||
| // Wins by being the composing-to channel. | ||
| eg.stream(name: 'Z', isRecentlyActive: false, streamWeeklyTraffic: null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This null case for traffic feels like kind of an odd edge case. I think this test would be simpler to think about if those null values were all replaced with 0 — it's clear that a channel with zero traffic should rank last on the traffic criterion.
test/model/autocomplete_test.dart
Outdated
| // Runner-up by having weekly traffic defined. | ||
| eg.stream(name: 'T', isRecentlyActive: false, streamWeeklyTraffic: 10), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And "weekly traffic defined" isn't really a criterion we use in the ranking — we're treating null equivalent to zero, after all. So I think this item can be dropped from the list.
gnprice
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, and here's a review of the next 3 commits:
7f03226 autocomplete test [nfc]: Use MarkedTextParse as the return type of parseMarkedText
6b2ef9c compose: Introduce PerAccountStore in ComposeController
ed7fda0 autocomplete: Identify when the user intends a channel link autocomplete
That leaves 5 commits still for the future:
c638335 autocomplete [nfc]: Add a TODO(#1967) for ignoring starting "**" after "#"
3d2b974 autocomplete test: Make setupToComposeInput accept channels param
1715d1b internal_link [nfc]: Factor out constructing fragment in its own method
36229c7 compose: Introduce fallbackMarkdownLink function
020c2bd channel: Finish channel link autocomplete for compose box
A couple of the more interesting comments below are about the autocompleteIntent tests. Please revise those tests to try to apply those points more generally: think through the user-facing behavior we're aiming to achieve and think of various cases where a lower-quality implementation might get an answer that's wrong from the user perspective, then write the test cases and comments to reflect those.
For examples of that structure, see the emoji tests below these tests. E.g. these are all about user-facing situations and reasons why users would want one behavior vs. another:
// Avoid interpreting colons in normal prose as queries.
// …
// Avoid interpreting already-entered `:foo:` syntax as queries.
// …
// Avoid interpreting emoticons as queries.(I think the tests for @-mentions above these are also designed that way — but they don't have comments narrating the story, so it's a bit harder to see that. The narrative is helpful.)
| _update(); | ||
| } | ||
|
|
||
| PerAccountStore store; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
compose: Introduce PerAccountStore in ComposeController
Instead can say:
compose: Move PerAccountStore up to ComposeController
That reflects the fact that although this commit adds a PerAccountStore reference on ComposeController, there already was one on the ComposeTopicController subclass.
That in turn helps reassure the reader by diminishing the scope of what's changing. When I see "introduce PerAccountStore", I have to worry: hmm, is that reference going to go out of date? Should we be doing something else to avoid keeping such a reference around? IOW, the sort of concerns that @chrisbobbe raised above at #1902 (comment) 🙂
When the verb is instead "move", though, then that's reassuring because it means that if there is such a problem it's not entirely a new problem. Also that we likely have some logic (as it turns out we do, per that thread) for dealing with the problem already, which hopefully resolves it (as indeed I think it does here).
In general, it's reassuring to have a given change be smaller rather than larger. So when you can write the commit message to clarify that's the case, with verbs like "move" or "extract" rather than "introduce" or "add", that's helpful.
lib/model/autocomplete.dart
Outdated
|
|
||
| extension ComposeContentAutocomplete on ComposeContentController { | ||
| int get _maxLookbackForAutocompleteIntent { | ||
| return 1 // intent character e.g., "#" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| return 1 // intent character e.g., "#" | |
| return 1 // intent character, e.g. "#" |
lib/model/autocomplete.dart
Outdated
| // To avoid spending a lot of time searching for autocomplete intents | ||
| // in long messages, we bound how far back we look for the intent's start. | ||
| final earliest = max(0, selection.end - 30); | ||
| final earliest = max(0, selection.end - _maxLookbackForAutocompleteIntent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This comment should move to _maxLookbackForAutocompleteIntent, as it's basically explaining what that's about.
lib/model/autocomplete.dart
Outdated
| // Also, make sure that the remaining query doesn't contain '**', | ||
| // otherwise '#**channel**' (which is a complete channel link syntax) and | ||
| // any text followed by that will always match. | ||
| + r'\*\*(?!\s)((?:(?!\*\*).)*)' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I worry this would be slow: it's doing lookahead ((?!…)) at every point in the string.
How about this version?
| + r'\*\*(?!\s)((?:(?!\*\*).)*)' | |
| + r'\*\*(?!\s)((?:[^*]|\*[^*])*)' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new version works for most cases, but not when there is * at the end of the query, for example, it will not pass the following test case:
doTest('~#**abc*^', channelLink('abc*'));I agree that this regex branch would be somewhat slower, but I think it will be less noticeable as we limit the number of characters we look for a match in. Also this regex branch will be executed less often than the other one (query starting without **).
Please let me know if I am missing something and we should avoid this slow regex branch. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Is that case desirable, though?
Seems like the main way it would come up is: you complete an autocomplete, then change your mind and start deleting the end of it in order to redo, and there's a moment where you've deleted one of the stars but not the other. This case checks that we end up with a query like abc* — but that's not a query the user wants in that situation, because the * is there only as a result of the #-mention syntax.
So I think in that situation, abc* is wrong; abc would be OK; and having no query is also OK, because the user can get to abc by just hitting backspace one more time.
If we did want to recognize a query abc* in that case, the regex can be adapted to do that by e.g. adding a branch \*$:
+ r'\*\*(?!\s)((?:[^*]|\*[^*]|\*$)*)'Also this regex branch will be executed less often than the other one (query starting without
**).
Hmm, on the contrary: after the user successfully uses this feature, I believe this branch will get invoked for each character they type after that. (Until they get to the max lookback distance after the #, which is probably around 100 characters after the end of the #-mention.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that case desirable, though?
(See also my comments on making these test cases more oriented to the user stories they serve: #1902 (comment) and #1902 (comment) below, and #1902 (review) above.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree.
One thing I was thinking about when writing that regex was how we can give users the freedom to use any characters in their queries (as long as they're allowed in the server), except for the characters and character patterns we use for our syntax. So if a user types #abc*, then we allow the query abc* to match for a * in the name. The same way in #**abc*. This will happen when the user starts deleting the end of a completed autocomplete, as you mentioned before, but one other case would be if the user starts modifying the query for a name that contains *, something like #**ad*.
Sorry if I am unnecessarily digging deeper on this one! 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree this is ideal:
give users the freedom to use any characters in their queries (as long as they're allowed in the server), except for the characters and character patterns we use for our syntax.
It's inherently limited, though, by that interaction with our markup syntax. And not only the channel-mention syntax but also the syntax that introduces other types of autocomplete interactions: if there's any of : or @ or # in the middle of the query you want to type so that the latter half of your query looks like a query in itself, then we'll go with that query instead.
So it's fine if there are some other limitations in a similar vein. I think rejecting #**abc* would be an example of that, because the trailing * looks like the start of a ** closing the channel-mention.
On the other hand, another thought that occurs to me now: where possible, it's a nice property to have if when you type more at the end of an autocomplete query, that filters down the results more narrowly but doesn't go in the other direction and cause results to start appearing again that didn't before. And we do accept #**abc*d as a query abc*d; so for that reason it'd be nice to similarly accept #**abc* as a query abc*. So that'd be a good reason to go for the slightly adapted version I posted at #1902 (comment) .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense. The recently pushed version includes the regex version you proposed in #1902 (comment), with a slight modification to disallow new-line characters in that branch.
lib/model/autocomplete.dart
Outdated
| // and typing "@#user" for the mention query "#user", because in 2025-11 | ||
| // channel and user name words can start with "#". (They can also contain "#" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| // and typing "@#user" for the mention query "#user", because in 2025-11 | |
| // channel and user name words can start with "#". (They can also contain "#" | |
| // and typing "@#user" for the mention query "#user", because as of 2025-11 | |
| // channel and user name words can start with "#". (They can also contain "#" |
Otherwise it sounds like you're suggesting it was different before, or will be different later. With "as of", it means it's this way now and you're just not saying anything about other times. The code is effectively presuming it's that way at all times.
test/model/autocomplete_test.dart
Outdated
| // #channel link. | ||
|
|
||
| doTest('^#', null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: put tests in same order as the code they test: @-mentions vs #-mentions vs emoji.
test/model/autocomplete_test.dart
Outdated
| // "#" sign can be (3 + 2 * maxChannelName) utf-16 code units | ||
| // away to the left of cursor. | ||
| doTest('If ~@chris^ is around, please ask him.', mention('chris'), maxChannelName: 10); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn't agree with the code — there's no "#".
test/model/autocomplete_test.dart
Outdated
| doTest(' ~#abc^', channelLink('abc')); | ||
| doTest('xyz ~#abc^', channelLink('abc')); | ||
|
|
||
| // Accept punctuations before channel link syntax. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| // Accept punctuations before channel link syntax. | |
| // Accept punctuation before channel link syntax. |
"Punctuation" is a mass noun, like "water".
test/model/autocomplete_test.dart
Outdated
| // Allow all other sorts of characters in query. | ||
| doTest('~#\u0000^', channelLink('\u0000')); // control | ||
| doTest('~#\u061C^', channelLink('\u061C')); // format character | ||
| doTest('~#\u0600^', channelLink('\u0600')); // format | ||
| doTest('~#\uD834^', channelLink('\uD834')); // leading surrogate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yuck. Are these really desirable? 🙂
We don't need to add logic to exclude these cases; I don't think they're important. But I'd like these tests to focus on the behavior we actually want, not on the behavior that our implementation happens to have by accident. That way we're not spending attention on thinking about these cases either in the implementation or the tests.
test/model/autocomplete_test.dart
Outdated
| // Two leading stars ('**') in the query are omitted. | ||
| doTest('~#**^', channelLink('')); | ||
| doTest('~#**abc^', channelLink('abc')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, the description of these test cases is focused on the logic in the implementation, and I'd like to instead focus on the user-facing goal which we want this code to accomplish and which this test verifies it accomplishes. Like this:
| // Two leading stars ('**') in the query are omitted. | |
| doTest('~#**^', channelLink('')); | |
| doTest('~#**abc^', channelLink('abc')); | |
| // Accept syntax like "#**foo" (as from the user finishing an autocomplete | |
| // and then hitting backspace to edit it), but leave the "**" out of the query. | |
| doTest('~#**^', channelLink('')); | |
| doTest('~#**abc^', channelLink('abc')); |
Right now, this is useful in how far back from the cursor we look to find a channel-link autocomplete (actually any autocomplete) interaction in compose box.
In the following commits, this will be used as one of the criteria for sorting channels in channel link autocomplete.
There are new changes made to `stream op: delete` event in server-10:
- The `streams` field which used to be an array of the just-deleted
channel objects is now an array of objects which only contains IDs
of the just-deleted channels (the app throws away its event queue
and reregisters before this commit).
- The same `streams` field is also deprecated and will be removed in a
future release.
- As a replacement to `streams`, `stream_ids` is introduced which is
an array of the just-deleted channels IDs.
Related CZO discussion:
https://chat.zulip.org/#narrow/channel/378-api-design/topic/stream.20deletion.20events/near/2284969
So to call AutocompleteViewManager.unregisterEmojiAutocomplete.
…iews` This set replaces the three sets of different `AutocompleteView` subclasses, simplifying the code.
These two methods were introduced but never called.
Also, generalize the dartdoc of NameMatchQuality. For almost all types of autocompletes, the matching mechanism/quality to an autocomplete query seems to be the same with rare exceptions (at the time of writing this —— 2025-11, only the emoji autocomplete matching mechanism is different).
020c2bd to
e907b90
Compare
|
Thanks @gnprice for the previous review. New changes pushed, PTAL. |
gnprice
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! These tests in particular look much clearer. Just small comments now, on the same portion of the branch I've previously reviewed:
bb8ef8e api: Add InitialSnapshot.maxChannelNameLength
a98b958 realm: Add RealmStore.maxChannelNameLength
f6b4212 api: Add ZulipStream.isRecentlyActive
148c5cf api: Update ChannelDeleteEvent to match new API changes
6ecc926 autocomplete: Call EmojiAutocompleteView.reassemble where needed
309b0f2 emoji: Add EmojiAutocompleteView.dispose
9f0ba09 autocomplete [nfc]: Introduce AutocompleteViewManager._autocompleteViews
edbea7c store: Call AutocompleteViewManager.handleUserGroupRemove/UpdateEvent
9dd48ed autocomplete [nfc]: Move _matchName up to AutocompleteQuery
e9408ae autocomplete: Add view-model ChannelLinkAutocompleteView
0a24f0a autocomplete test [nfc]: Use MarkedTextParse as the return type of parseMarkedText
e58c075 compose: Move PerAccountStore up to ComposeController
21af790 autocomplete: Identify when the user intends a channel link autocomplete
That leaves 5 commits still ahead:
8f380f9 autocomplete [nfc]: Add a TODO(#1967) for ignoring starting "**" after "#"
1e4b228 autocomplete test: Make setupToComposeInput accept channels param
b4cf343 internal_link [nfc]: Factor out constructing fragment in its own method
39293d8 compose: Introduce fallbackMarkdownLink function
e907b90 channel: Finish channel link autocomplete for compose box
test/model/autocomplete_test.dart
Outdated
| test('favor channel with emoji-prefixed name', () { | ||
| prepare(); | ||
| checkPrecedes('🗄️backend', 'announce'); | ||
| checkPrecedes('🗄️ BACKEND', 'annouce'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| checkPrecedes('🗄️ BACKEND', 'annouce'); | |
| checkPrecedes('🗄️ BACKEND', 'announce'); |
| test('favor channel with emoji-prefixed name', () { | ||
| prepare(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test can benefit from a short comment explaining what story it's meant to tell:
| test('favor channel with emoji-prefixed name', () { | |
| prepare(); | |
| test('favor channel with emoji-prefixed name', () { | |
| // This checks that channel names are being compared with the | |
| // proper method ChannelStore.compareChannelsByName, | |
| // rather than a more naïve string comparison. | |
| prepare(); |
| // For a channel name, server excludes a wide range of characters and | ||
| // code points in `\p{C}` major category, namely the minor catories `\p{Cc}`, | ||
| // `\p{Cs}`, and part of `\p{Cn}`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit inverted:
| // For a channel name, server excludes a wide range of characters and | |
| // code points in `\p{C}` major category, namely the minor catories `\p{Cc}`, | |
| // `\p{Cs}`, and part of `\p{Cn}`. | |
| // In a channel name, the server accepts a wide range of characters. | |
| // It excludes only portions of the `\p{C}` major category, | |
| // namely the minor categories `\p{Cc}`, `\p{Cs}`, and part of `\p{Cn}`. |
| // What's likely to come just before #channel syntax: the start of the string, | ||
| // whitespace, or punctuation. Letters are unlikely; in that case a GitHub- | ||
| // style "zulip/zulip-flutter#124" link might be intended (as on CZO where | ||
| // there's a custom linkifier for that). | ||
| // | ||
| // By punctuation, we mean *some* punctuation, like "(". We make "#" and "@" | ||
| // exceptions, to support typing "##channel" for the channel query "#channel", | ||
| // and typing "@#user" for the mention query "#user", because as of 2025-11 | ||
| // channel and user name words can start with "#". They can also contain "#" | ||
| // anywhere else in the name; we handle that partially and do not match for | ||
| // all the cases because of how we match for a query in the name. | ||
| // See the related discussion: | ||
| // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/channel.20autocomplete.3A.20channels.20with.20.22.23.22.20in.20name/near/2288883 | ||
| const before = r'(?<=^|\s|\p{Punctuation})(?<![#@])'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a tightened version:
| // What's likely to come just before #channel syntax: the start of the string, | |
| // whitespace, or punctuation. Letters are unlikely; in that case a GitHub- | |
| // style "zulip/zulip-flutter#124" link might be intended (as on CZO where | |
| // there's a custom linkifier for that). | |
| // | |
| // By punctuation, we mean *some* punctuation, like "(". We make "#" and "@" | |
| // exceptions, to support typing "##channel" for the channel query "#channel", | |
| // and typing "@#user" for the mention query "#user", because as of 2025-11 | |
| // channel and user name words can start with "#". They can also contain "#" | |
| // anywhere else in the name; we handle that partially and do not match for | |
| // all the cases because of how we match for a query in the name. | |
| // See the related discussion: | |
| // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/channel.20autocomplete.3A.20channels.20with.20.22.23.22.20in.20name/near/2288883 | |
| const before = r'(?<=^|\s|\p{Punctuation})(?<![#@])'; | |
| // What's likely to come just before #channel syntax: the start of the string, | |
| // whitespace, or punctuation. Letters are unlikely; in that case a GitHub- | |
| // style "zulip/zulip-flutter#124" link might be intended (as on CZO where | |
| // there's a custom linkifier for that). | |
| // | |
| // Only some punctuation, like "(", is actually likely here. We don't | |
| // currently try to be specific about that, except we exclude "#" and "@" | |
| // in order to allow typing "##channel" for the channel query "#channel" | |
| // and "@#user" for the mention query "#user". See discussion: | |
| // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/channel.20autocomplete.3A.20channels.20with.20.22.23.22.20in.20name/near/2288883 | |
| const before = r'(?<=^|\s|\p{Punctuation})(?<![#@])'; |
That makes a little less that we're asking the reader to get through and understand in order to see what's going on here. The exclusion of "#" and "@" here is a pretty marginal feature, and going deep into the ins and outs of it has a maintenance cost in the form of more complexity for the next person to understand.
Also this rewords the "we mean some punctuation" sentence to clarify what it's trying to say. (That wording looks to be copied from the corresponding comment in _mentionIntentRegex above, where this is what it meant.)
test/model/autocomplete_test.dart
Outdated
| // Link syntax can come after punctuation. | ||
| doTest(':~#abc^', channelLink('abc')); | ||
| doTest('!~#abc^', channelLink('abc')); | ||
| doTest(',~#abc^', channelLink('abc')); | ||
| doTest('.~#abc^', channelLink('abc')); | ||
| doTest('(~#abc^', channelLink('abc')); doTest(')~#abc^', channelLink('abc')); | ||
| doTest('{~#abc^', channelLink('abc')); doTest('}~#abc^', channelLink('abc')); | ||
| doTest('[~#abc^', channelLink('abc')); doTest(']~#abc^', channelLink('abc')); | ||
| doTest('“~#abc^', channelLink('abc')); doTest('”~#abc^', channelLink('abc')); | ||
| doTest('«~#abc^', channelLink('abc')); doTest('»~#abc^', channelLink('abc')); | ||
| // … and other punctuation except for '#' and '@', because they start |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: an ellipsis "…" at the start of a comment means it's continuing a sentence from the previous comment — so that previous comment should end with an ellipsis too, not a period.
| // Link syntax can come after punctuation. | |
| doTest(':~#abc^', channelLink('abc')); | |
| doTest('!~#abc^', channelLink('abc')); | |
| doTest(',~#abc^', channelLink('abc')); | |
| doTest('.~#abc^', channelLink('abc')); | |
| doTest('(~#abc^', channelLink('abc')); doTest(')~#abc^', channelLink('abc')); | |
| doTest('{~#abc^', channelLink('abc')); doTest('}~#abc^', channelLink('abc')); | |
| doTest('[~#abc^', channelLink('abc')); doTest(']~#abc^', channelLink('abc')); | |
| doTest('“~#abc^', channelLink('abc')); doTest('”~#abc^', channelLink('abc')); | |
| doTest('«~#abc^', channelLink('abc')); doTest('»~#abc^', channelLink('abc')); | |
| // … and other punctuation except for '#' and '@', because they start | |
| // Link syntax can come after punctuation… | |
| doTest(':~#abc^', channelLink('abc')); | |
| doTest('!~#abc^', channelLink('abc')); | |
| doTest(',~#abc^', channelLink('abc')); | |
| doTest('.~#abc^', channelLink('abc')); | |
| doTest('(~#abc^', channelLink('abc')); doTest(')~#abc^', channelLink('abc')); | |
| doTest('{~#abc^', channelLink('abc')); doTest('}~#abc^', channelLink('abc')); | |
| doTest('[~#abc^', channelLink('abc')); doTest(']~#abc^', channelLink('abc')); | |
| doTest('“~#abc^', channelLink('abc')); doTest('”~#abc^', channelLink('abc')); | |
| doTest('«~#abc^', channelLink('abc')); doTest('»~#abc^', channelLink('abc')); | |
| // … except for '#' and '@', because they start |
(Also the "and other" part here doesn't really fit — other than what?)
test/model/autocomplete_test.dart
Outdated
| // Avoid interpreting as queries when # apears in links. | ||
| doTest('zulip/zulip-flutter#124^', null); | ||
| doTest('https://example.com/docs#installation^', null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: only one of these is a "link" in a canonical way; here's a rewording:
| // Avoid interpreting as queries when # apears in links. | |
| doTest('zulip/zulip-flutter#124^', null); | |
| doTest('https://example.com/docs#installation^', null); | |
| // Avoid interpreting as queries a URL or a common linkifier syntax. | |
| doTest('https://example.com/docs#installation^', null); | |
| doTest('zulip/zulip-flutter#124^', null); |
gnprice
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here's a partial review of those remaining 5 commits, before I head off for the holiday.
| return result; | ||
| } | ||
|
|
||
| String narrowLinkFragment(PerAccountStore store, Narrow narrow, {int? nearMessageId}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting point.
I'm looking at other narrowLink call sites to see if any of them should also use this — i.e. should generate realm-relative URLs for narrows, rather than absolute URLs.
It looks like there's a pair of such call sites already: quoteAndReply and its placeholder counterpart. So that'd be good to fix.
lib/model/compose.dart
Outdated
| /// these characters. | ||
| // Corresponds to `topic_link_util.get_fallback_markdown_link` in Zulip web; | ||
| // https://github.com/zulip/zulip/blob/b42d3e77e/web/src/topic_link_util.ts#L96-L108 | ||
| String fallbackMarkdownLink({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name seems too general: this is specifically making a link to a channel or topic, not just any link (or any link in normal Markdown syntax).
lib/widgets/autocomplete.dart
Outdated
| if (query is! ChannelLinkAutocompleteQuery) { | ||
| return; // Shrug; similar to `intent == null` case above. | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for this, since we don't try to use any details from query below.
As of this commit, it's not yet possible in the app to initiate a channel link autocomplete interaction. So in the widgets code that would consume the results of such an interaction, we just throw for now, leaving that to be implemented in a later commit.
This way, all subclasses can use the reference to the store for different purposes, such as using `max_topic_length` for the topic length instead of the hard-coded limit of 60, or using `max_stream_name_length` for how far back from the cursor we look to find a channel-link autocomplete interaction in compose box.
For this commit we temporarily intercept the query at the AutocompleteField widget, to avoid invoking the widgets that are still unimplemented. That lets us defer those widgets' logic to a separate later commit.
This will make it easy to use the fragment string in several other places, such as in the next commits where we need to create a fallback markdown link for a channel.
e907b90 to
b55970e
Compare
Also, use the same URL type in quoteAndReplyPlaceholder. In inline links, absolute and relative realm URLs are treated the same by web and mobile, so better to use the shorter version.
b55970e to
dff4817
Compare
|
Thanks for the review. New revision pushed. |
Fixes-partly: #124 (topic link autocomplete will be its own PR)
Screenshots
Screen recording
Channel.autocomplete.recording.mov